Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.

...powered by www.netzwerkartist.de...

 << zurück
Visual C# 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2005

Visual C# 2005
1.320 S., mit 2 CDs, 59,90 Euro
Galileo Computing
ISBN 3-89842-586-X
gp Kapitel 26 Datenbankzugriff mit ADO.NET
  gp 26.1 Eine kleine Einführung
  gp 26.2 Die Verbindung zu einer Datenquelle herstellen
    gp 26.2.1 Der Inhalt der Verbindungszeichenfolge
    gp 26.2.2 Die Authentifizierung
    gp 26.2.3 Das Öffnen einer Verbindung
    gp 26.2.4 Schließen einer Verbindung
    gp 26.2.5 Die Dauer des Verbindungsaufbaus
    gp 26.2.6 Eigenschaften eines »Connection«-Objekts
    gp 26.2.7 Die Ereignisse eines »Connection«-Objekts
    gp 26.2.8 Unterstützung bei Projekten mit grafischer Benutzeroberfläche
  gp 26.3 Das »Command«-Objekt
    gp 26.3.1 Erzeugen eines »Command«-Objekts
    gp 26.3.2 Ausführen des »Command«-Objekts
    gp 26.3.3 Aktionsabfragen mit »ExecuteNonQuery« absetzen
    gp 26.3.4 Auswahlabfragen mit »ExecuteReader«
    gp 26.3.5 Abfragen, die genau ein Ergebnis liefern
    gp 26.3.6 Parametrisierte Abfragen
    gp 26.3.7 Die Unterstützung des Visual Studios 2005
  gp 26.4 Der »DataAdapter« als Bindeglied zwischen Datenbank und verbindungslosen Objekten
    gp 26.4.1 Die Konstruktoren der Klasse »DataAdapter«
    gp 26.4.2 Den lokalen Datenspeicher mit der Methode »Fill« füllen
    gp 26.4.3 Abrufen von Schemainformationen
    gp 26.4.4 Die Unterstützung des Visual Studios 2005
  gp 26.5 Das »DataSet«-Objekt
    gp 26.5.1 Ein »DataSet«-Objekt erzeugen
    gp 26.5.2 Das »DataSet« füllen
    gp 26.5.3 Tabellen- und Spaltenbezeichner zuordnen
  gp 26.6 »DataTable«-Objekte
    gp 26.6.1 Die Zeilen und Spalten in einer »DataTable«
    gp 26.6.2 Mit mehreren Tabellen arbeiten
    gp 26.6.3 Änderungen an einer »DataTable« vornehmen
    gp 26.6.4 Datenausgabe in WinForms mit dem Visual Studio 2005
  gp 26.7 Aktualisieren der Datenbank
    gp 26.7.1 Aktualisieren mit dem »CommandBuilder«-Objekt
    gp 26.7.2 Manuell gesteuerte Aktualisierungen
    gp 26.7.3 Aktualisieren mit »ExecuteNonQuery«
    gp 26.7.4 Manuelles Aktualisieren mit dem DataAdapter
    gp 26.7.5 Den zu aktualisierenden Datensatz in der Datenbank suchen
    gp 26.7.6 Den Benutzer über die fehlgeschlagenen Aktualisierungen informieren
    gp 26.7.7 Die konfliktverursachenden Datenzeilen bei der Datenbank abfragen


Galileo Computing

26.7 Aktualisieren der Datenbandowntop


Galileo Computing

26.7.1 Aktualisieren mit dem »CommandBuilder«-Objekt  downtop

Eine DataTable können Sie mit Daten aus jeder Datenquelle füllen. Handelt es sich dabei um eine Datenbank und können die Benutzer die Daten auch ändern, müssen die Änderungen zu einem bestimmten Zeitpunkt an die Datenbank übermittelt werden. Des Öfteren habe ich bereits die Update-Methode des DataAdapters erwähnt, die eine Verbindung zu der Datenbank aufbaut, um deren Datenbestand zu aktualisieren. Vielleicht haben Sie auch schon die Update-Methode ausgetestet, nachdem Sie Zeilen Ihres DataSets geändert hatten. Sie werden dabei bestimmt einen Laufzeitfehler erhalten haben. Wenn nicht, ergänzen Sie den Code des Beispiels DataRowVersionDemo wie gezeigt:


...
// Ausgabe der geänderten Datenzeilen
foreach (DataRow editRow in tbl.Rows)
  if (editRow.RowState == DataRowState.Added)
    Console.WriteLine("Autor {0} – Added", editRow["au_lname"]);
  else if (editRow.RowState == DataRowState.Modified)
    Console.WriteLine("Autor {0} – Modified", ditRow["au_lname"]);
  else if (editRow.RowState == DataRowState.Deleted)
    Console.WriteLine("Autor {0} – Deleted", 
       editRow["au_lname", DataRowVersion.Original]);
// Datenbank aktualisieren
da.Update(ds);

Wo liegt aber die Ursache des nun auftretenden Laufzeitfehlers?

Denken wir einmal daran, wie die Abfolge ist, bis der DataAdapter eine Auswahlabfrage an die Datenbank schickt. Wir hatten ein Command-Objekt erzeugt und diesem das SELECT-Statement übergeben. Bei der Instanziierung gaben wir dem DataAdapter das Command-Objekt über den Konstruktoraufruf bekannt. Der DataAdapter speichert das in seiner Eigenschaft SelectCommand.

Der DataAdapter hat aber noch drei weitere Eigenschaften, die nach einem Command-Objekt verlangen:

gp  InsertCommand
gp  DeleteCommand
gp  UpdateCommand

So wie über SelectCommand die vom DataAdapter abzusetzende Auswahlabfrage bekannt ist, benötigt der Adapter auch noch Command-Objekte, welche die Statements INSERT, DELETE und UPDATE beschreiben.

Selbstständig wird der DataAdapter dieses Aktualisierungsstatement nicht bereitstellen, obwohl er das durchaus könnte. Denn anhand der SELECT-Abfrage ließe sich auch nach festgeschriebenen Kriterien das Aktualisieren eigenständig in die Hand nehmen. Dann hätten wir jedoch mit einem riesengroßen Nachteil zu leben, denn uns würde eine konkrete Aktualisierungslogik vorgeschrieben, ohne uns die Freiheit zu geben, selbst Einfluss darauf ausüben zu können. Einfluss auf die Aktualisierung ist aber dann notwendig, wenn wir Parallelitätskonflikte erwarten können. Mit anderen Worten: Andere Benutzer haben zuvor Änderungen am gleichen Datensatz vorgenommen. Dann wollen wir aber die Gewähr haben, dass Konflikte auftreten, wenn es den Anforderungen von Anwendung und Datenbank entspricht. Den Konflikten werden wir uns zum Abschluss des Kapitels zuwenden.

Der DataAdapter erzeugt also automatisch keine Aktualisierungslogik, die Eigenschaften Insert-, Update- und DeleteCommand haben den Inhalt null. Mit dieser Erkenntnis haben wir aber schon das Tor der Aktualisierung aufgeschlossen, egal ob wir eine manuell oder automatisch generierte Aktualisierungslogik anstreben. Zuerst wollen wir die automatische besprechen.

Zur Generierung von Befehlen gibt es ein providerspezifisches Objekt. Es ist der SqlCommandBuilder. Bei der Instanziierung übergeben wir dem Konstruktor die Referenz auf unseren SqlDataAdapter.


SqlCommandBuilder cmb = new SqlCommandBuilder(da);

Da der SqlCommandBuilder über das SqlDataAdapter-Objekt weiß, wie die SELECT-Auswahlabfrage aussieht, erzeugt er die Befehle INSERT, DELETE und UPDATE auf dieser Grundlage, weist sie neuen Command-Objekten zu und übergibt diese schließlich den Eigenschaften UpdateCommand, InsertCommand und DeleteCommand des SqlDataAdapters.

Damit ist der DataAdapter auf alle Eventualitäten vorbereitet. Unabhängig davon, ob im DataSet eine Zeile gelöscht, hinzugefügt oder editiert worden ist, wird die Originaldatenbank anhand der Kommandos aktualisiert.

Kommen wir zurück zu dem eingangs gezeigten Codefragment. Wenn Sie vor dem Aufruf von Update wie gezeigt ein SqlCommandBuilder-Objekt erzeugen, wird die Aktualisierung erfolgreich sein.


...
SqlCommandBuilder cmb = new SqlCommandBuilder(da);
da.Update(ds);

Die von »CommandBuilder« generierten Aktualisierungsstatements

Dass ein CommandBuilder Aktualisierungscode anhand des SELECT-Statements erzeugt, habe ich oben erwähnt. Doch wie sieht der Code exakt aus?

Wir wollen uns das nun noch kurz ansehen. Grundlage dazu bildet die Abfrage:


SELECT au_id, au_lname, au_fname, contract FROM authors

Sie können sich die Aktualisierungsstatements ausgeben lassen, indem Sie die Methoden GetUpdateCommand, GetInsertCommand oder GetDeleteCommand des CommandBuilders aufrufen. Alle liefern ein Objekt vom Typ SqlCommand, über dessen Eigenschaft CommandText Sie das jeweilige SQL-Statement abfragen können. Es genügt, wenn wir uns nur eines der drei ansehen.


UPDATE [authors]
SET [au_id] = @p1, [au_lname] = @p2, [au_fname] = @p3, [contract] = @p4
WHERE (([au_id] = @p5) AND ([au_lname] = @p6) 
      AND ([au_fname] = @p7) AND ([contract] = @p8))

Sie können erkennen, dass hinter der WHERE-Klausel alle Spalten der SELECT-Abfrage als Suchkriterium nach dem zu editierenden Datensatz aufgeführt sind. Die Parameter @p5 bis @p8 werden mit den Daten gefüllt, die unter DataRowVersion.Original aus dem Dataset bezogen werden, @p1 bis @p4 erhalten die Daten aus DataRowVersion.Current. In gleicher Weise werden auch die INSERT- und DELETE-Statements vom CommandBuilder generiert.


Galileo Computing

26.7.2 Manuell gesteuerte Aktualisierungen  downtop

Das Aktualisieren einer Datenquelle mit dem CommandBuilder-Objekt ist sehr einfach. Aber diese Einfachheit hat ihren Preis, denn wir müssen uns mit den Charakteristiken des Objekts abfinden und haben keinen Einfluss darauf, wie die Daten zurückgeschrieben werden, denn der CommandBuilder generiert Abfragen, die zur Identifikation einer Datenzeile in der Tabelle einer Datenbank alle Spalten einschließt, die mit SELECT abgefragt worden sind.

Was passiert nun, wenn der SqlDataAdapter eine Datenzeile aktualisieren möchte, die ein anderer Benutzer zwischenzeitlich geändert hat? Nehmen wir an, Anwender A hätte die Tabelle authors in ein DataSet geladen und den Datensatz des Autors White editiert. Anwender B hat, nachdem Anwender A die angeforderten Daten erhalten hat, die gleichen Daten abgefragt. Anwender B aktualisiert ebenfalls Daten des Autors White und speichert die Änderungen in der Originaldatenquelle. Versucht anschließend Anwender A seine Daten in der Datenbank zu speichern, kann der DataAdapter den Datensatz nicht mehr finden, denn einer der Spalteneinträge hat sich bekanntlich durch Anwender B geändert. Die Folge ist, dass die Änderungen durch Anwender A nicht von der Datenbank übernommen werden. Es kommt zu einem Parallelitätskonflikt.

Möglicherweise entspricht das den Anforderungen an die Datenbankanwendung. Wie müssen Entwickler aber vorgehen, wenn ein »Last in wins«-Aktualisierungsszenario gefordert ist, in dem grundsätzlich immer derjenige, der als Letzter eine Datenzeile zurückschreibt, die ihm unbekannten Änderungen durch einen anderen Anwender überschreiben soll?

Es gibt auch noch andere denkbare Ansätze, wann ein Konflikt auftreten soll bzw. wann Änderungen an dem gleichen Datensatz durch zwei oder mehr Benutzer gleichermaßen akzeptiert werden können. Stellen wir uns dazu nur vor, Anwender A hätte das Feld au_fname des Autors White geändert, weil ihm aufgefallen ist, dass der Vorname in der Tabelle falsch eingetragen ist. Anwender B wiederum könnte die Mitteilung erhalten haben, dass sich die Telefonnummer desselben Autors geändert hat. Sie werden mir zustimmen, dass im Grunde genommen beide mit ihren Änderungen zu ihrem Recht kommen müssten, weil beide begründet sind.

Mit den beiden letztgenannten Szenarien wird der CommandBuilder nicht umgehen können. Er scheidet deshalb aus. Wir müssen selbst die gewünschte Aktualisierung formulieren. Vom Ansatz her ist das sehr einfach. Erinnern wir uns nur, welche Aufgabe der CommandBuilder hat: Er erzeugt auf Basis der SELECT-Abfrage SQL-Statements für das Editieren (UPDATE), Einfügen (INSERT INTO) und Löschen (DELETE) einer Datenzeile Command-Objekte, die er an den DataAdapter weitergibt.

Wollen Sie ein eigene Aktualisierungsszenario beschreiben, lassen Sie den CommandBuilder aus dem Spiel und stellen eigene Command-Objekte bereit, deren SQL-Abfragestatements sich an den aktuellen Forderungen orientieren. Hier können Sie zwei Ansätze verfolgen:

gp  Sie rufen für jede geänderte Datenzeile die ExecuteNonQuery-Methode des Command-Objekts auf. Dazu müssen Sie Code bereitstellen, der alle geänderten Datenzeilen einer DataTable erfasst.
gp  Sie überlassen weiterhin dem DataAdapter die Aufgabe, in der DataTable nach geänderten Datenzeilen zu suchen und diese der Reihe nach an die Datenbank zu übermitteln.

Im Folgenden werde ich Ihnen beide Alternativlösungen zum CommandBuilder vorstellen.


Galileo Computing

26.7.3 Aktualisieren mit »ExecuteNonQuery«  downtop

Bereitstellung der Command-Objekte

Ehe wir uns ansehen, wie mit ExecuteNonQuery des Command-Objekts eine anwendungsspezifische Aktualisierung implementiert werden kann, wollen wir die Forderung spezifizieren. Dabei soll hier exemplarisch die schon bekannte Tabelle authors die Daten bereitstellen, die wir ändern wollen.

Gehen wir also davon aus, dass immer die letzte Änderung Vorrang vor allen anderen haben soll. Es ist die Formulierung des »Last in wins«-Szenarios. Eine in einer DataTable geänderte Datenzeile muss, wenn sie an die Originaldatenbank übermittelt werden soll, dort auch identifiziert werden können. Dazu dienen die Primärschlüsselspalten. Das UPDATE-Statement könnte dann beispielsweise wie folgt aussehen:


UPDATE authors SET au_id = x, au_lname = y, au_fname = z, ...
WHERE au_id = abcd

Findet der Aktualisierungsprozess in der Originaltabelle authors den Datensatz mit dem Primärschlüssel abcd, trägt er alle hinter SET aufgeführten Spalteninhalte ein. Dabei interessiert nicht im Geringsten, ob sich die Inhalte seit dem Generieren des DataSets auf Benutzerseite geändert haben oder nicht. Beachten Sie, dass sich dieses UPDATE-Statement deutlich von dem, was ein CommandBuilder erzeugen würde, unterscheidet.

Das Vorhaben war, jede geänderte Datenzeile mit ExecuteNonQuery zu übermitteln. Also benötigen wir zuerst ein Command-Objekt, das die parametrisierte Abfrage beschreibt. Dafür schreiben wir eine Methode.

 


// UpdateCommand erzeugen
public SqlCommand GetUpdateCommand(SqlConnection con) {
  string strSQL = "UPDATE authors SET au_id=@IDNew, ";
  strSQl += "au_lname=@Zuname, au_fname=@Vorname WHERE au_id=@ID";
  SqlCommand cmd = new SqlCommand(strSQL, con);
  // die Parameter der Parameters-Auflistung hinzufügen
  cmd.Parameters.Add("@IDNew", SqlDbType.VarChar, 11);
  cmd.Parameters.Add("@Zuname", SqlDbType.VarChar, 40);
  cmd.Parameters.Add("@Vorname", SqlDbType.VarChar, 20);
  cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11);
  return cmd;
}

Um den Code übersichtlich zu halten, enthält UPDATE nur die Angabe der drei Spalten au_id, au_lname und au_fname. Diese werden, wie auch die Angabe des Suchkriteriums, als Parameter des Command-Objekts definiert. Die Methode GetUpdateCommand liefert dem aufrufenden Code ein SqlCommand als Rückgabewert, das vollständig ausgebildet ist.

Ehe mit einer Anweisung wie


myCommand.ExecuteNonQuery();

aktualisiert werden kann, müssen die Parameter gefüllt werden. Dazu dient eine eigene Methode. Weil nach dem Füllen der Parameter das Command-Objekt ausführungsfertig ist, kann sogar schon innerhalb der Methode ExecuteNonQuery aufgerufen werden. Dazu muss der Methode neben dem von GetUpdateCommand generierten Command-Objekt nur die zu aktualisierende Datenzeile bekannt sein. Beide übergeben wir der Parameterliste der Methode, die wir SubmitUpdateRow nennen.

Der Aufruf von ExecuteNonQuery liefert eine Zahl zurück, die Auskunft darüber gibt, wie viele Datenzeilen aktualisiert werden konnten. Darauf wollen wir nicht verzichten, denn Sie kann uns dabei behilflich sein, etwaige Konflikte, die beim Aktualisieren auftreten, zu behandeln. Stellen Sie sich dazu nur vor, ein anderer Anwender hätte zwischenzeitlich den betreffenden Datensatz gelöscht. Der Rückgabewert von ExecuteNonQuery und damit auch unserer benutzerdefinierten Methode wäre 0.


public int SubmitUpdatedRow(SqlCommand cmd, DataRow row) {
  // Parameter füllen
  cmd.Parameters["IDNew"].Value = row["au_id", DataRowVersion.Current];
  cmd.Parameters["@Zuname"].Value = row["au_lname", DataRowVersion.Current];
  cmd.Parameters["@Vorname"].Value = row["au_fname",DataRowVersion.Current];
  cmd.Parameters["@ID"].Value = row["au_id",DataRowVersion.Original];
  // Anzahl der betroffenen Zeilen zurückliefern
  return cmd.ExecuteNonQuery();
}

Beim Füllen der Parameter müssen Sie bedenken, welche Datenversion Sie in den jeweiligen Parameter eintragen müssen. Die hinter SET aufgeführten Spalten beschreiben die zu ändernden Werte. Es sind also die, die unter DataRowVersion.Current der entsprechenden Datenzeile für die Spalte zu finden sind. Übergeben Sie der Eigenschaft Value des Parameters beispielsweise mit


row["au_fname"]

einen Wert, wird diese Version automatisch genommen. Um den Code besser lesbar zu machen, habe ich aber ausdrücklich DataRowVersion mit angegeben.

Etwas anders ist der Sachverhalt, wenn es darum geht, das Suchkriterium für die Aktualisierung festzulegen. Bedenken Sie, dass die Primärschlüsselspalte der Tabelle authors grundsätzlich editierbar ist und nicht ausgeschlossen werden könnte, dass der Anwender auch hier eine Änderung vorgenommen hat. Die entsprechende Datenzeile kann aber nur dann gefunden werden, wenn der Originalwert übermittelt wird. Deshalb ist es unerlässlich, dass die hinter WHERE angegebenen Spalten immer DataRowVersion.Original-Werte enthalten.

In ähnlicher Weise können Sie auch Command-Objekte bereitstellen, die gelöschte und hinzugefügte Datenzeilen beschreiben.


// DeleteCommand erzeugen
public SqlCommand GetDeleteCommand(SqlConnection con) {
  string strSQL = "DELETE FROM authors WHERE au_id=@ID";
  SqlCommand cmd = new SqlCommand(strSQL, con);
  // die Parameter der Parameters-Auflistung hinzufügen
  cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11);
  return cmd;
}
// InsertCommand erzeugen
public SqlCommand GetInsertCommand(SqlConnection con) {
  string strSQL = "INSERT INTO authors (au_id, au_lname, au_fname, contract) ";
  strSQL += "Values(@ID, @Zuname, @Vorname, @Contract)";
  SqlCommand cmd = new SqlCommand(strSQL, con);
  // die Parameter der Parameters-Auflistung hinzufügen
  cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11);
  cmd.Parameters.Add("@Zuname", SqlDbType.VarChar, 40);
  cmd.Parameters.Add("@Vorname", SqlDbType.VarChar, 20);
  cmd.Parameters.Add("@Contract", SqlDbType.Bit);
  return cmd;
}
public int SubmitDeletedRow(SqlCommand cmd, DataRow row) {
  // Parameter füllen
  cmd.Parameters["@ID"].Value = row["au_id", DataRowVersion.Original];
  return cmd.ExecuteNonQuery();
}
public int SubmitAddedRow(SqlCommand cmd, DataRow row) {
  // Parameter füllen
  cmd.Parameters["@ID"].Value = row["au_id"];
  cmd.Parameters["@Zuname"].Value = row["au_lname"];
  cmd.Parameters["@Vorname"].Value = row["au_fname"];
  cmd.Parameters["@Contract"].Value = row["contract"];
  return cmd.ExecuteNonQuery();
}

Alle Änderungen an die Datenquelle übermitteln

Nun müssen wir uns überlegen, wie wir diese drei Methodenpaare benutzen können. Setzen wir voraus, dass wir eine zur Aktualisierung anstehende Datenzeile über row referenzieren, könnte der Aufruf wie folgt lauten:


SqlCommand cmdUpdate = GetUpdateCommand(con); 
int countDS = SubmitUpdatedRow(cmdUpdate, row);
if (countDS == 1)
  // Aktualisierung ist gelungen

Damit ist der Grundstock gelegt. Aber wie kommen wir an die Referenz einer geänderten DataRow?

Liegt ein DataSet vor, das viele Datenzeilen enthält, von denen wir nicht wissen, ob eine einzelne Datenzeile geändert, hinzugefügt oder gelöscht worden ist, gilt es, einen Weg zu finden, um die Datensätze daraufhin zu untersuchen. Hier bietet sich eine Überladung der Methode Select der DataTable an.


public DataRow[] Select(string, string, DataRowViewState);

Der erste Parameter beschreibt eine Zeichenfolge zum Filtern der Datenzeilen, der zweite eine Zeichenfolge für die Sortierrichtung. Der dritte Parameter vom Typ DataRowViewState ist der, der uns die Lösung bietet. DataRowViewState ist eine Enumeration, deren Member bitweise kombiniert werden können.


Tabelle 26.7   Member-Liste der Enumeration »DataRowViewState« (Auszug)

Member Beschreibung
Added Beschreibt eine hinzugefügte Datenzeile.
Deleted Beschreibt eine gelöschte Datenzeile.
ModifiedCurrent Beschreibt eine geänderte Datenzeile.

Wir können der Select-Methode der DataTable angeben, nach welchen Datenzeilenversionen wir suchen wollen. Handelt es sich um gelöschte, hinzugefügte und editierte, fassen wir das Suchkriterium in einer Variablen zusammen:


DataViewRowState drvs = DataViewRowState.Added | 
                        DataViewRowState.Deleted | 
                        DataViewRowState.ModifiedCurrent;

Select liefert ein Array von den Datenzeilen, die sich in irgendeiner Weise vom Original in der Datenbank unterscheiden. Das Array durchlaufen wir elementweise und prüfen dabei jeweils die Eigenschaft RowState der aktuellen DataRow. Je nachdem, ob es sich um eine gelöschte, editierte oder hinzugefügte Zeile handelt, reagieren wir mit dem Aufruf einer unserer drei SubmitXxx-Methoden.


foreach (DataRow row in ds.Tables[0].Select("", "", drvs)) {
  switch (row.RowState) {
    case DataRowState.Added:
      countDS = SubmitAddedRow(cmdInsert, row);
      break;
    case DataRowState.Deleted:
      countDS = SubmitDeletedRow(cmdDelete, row);
      break;
    case DataRowState.Modified:
      countDS = SubmitUpdatedRow(cmdUpdate, row);
      break;
  }
  if (countDS == 1)
    // Aktualisierung war erfolgreich
  else
    // Aktualisierung ist fehlgeschlagen}

Das DataSet mit »AcceptChanges« aktualisieren

Der Aufruf der Methode ExecuteNonQuery liefert uns als Ergebnis des Aufrufs eine Zahl, welche die Anzahl der betroffenen Datensätze angibt. Da wir für jede einzelne Datenzeile die Methode aufrufen, wird uns die Rückgabe der Zahl 1 signalisieren, ob unser Aktualisierungsversuch von der Originaldatenquelle akzeptiert worden ist. Für uns ist dieses Ergebnis von elementarer Bedeutung; denn wir haben den Aktualisierungsprozess selbst in die Hand genommen und müssen ihn nun auch noch zu einem erfolgreichen Ende bringen.

Die Update-Methode des DataAdapters sorgt automatisch dafür, dass das DataSet nach erfolgreicher Aktualisierung auf den aktuellen Stand gebracht wird. Dazu werden die Daten, die unter DataRowVersion.Original zu finden sind, durch die Daten in DataRowVersion.Current ersetzt. Außerdem wird der RowState der betreffenden Datenzeile angepasst: Zeilen, die Added oder Modified sind, werden auf Unchanged gesetzt. Gelöschte Zeilen werden endgültig aus dem DataSet entfernt.

Ist die Aktualisierung mit ExecuteNonQuery erfolgreich verlaufen, müssen wir das unsererseits ebenfalls im DataSet mitteilen, um den letzten Originalzustand im DataSet widerzuspiegeln. Es gilt demnach, den DataRowState der betreffenden Datenzeilen auf DataRowState.Unchanged zu setzen beziehungsweise erfolgreich gelöschte Datenzeilen aus dem DataSet zu entfernen. Zudem muss bei allen geänderten Datenzeilen DataRowVersion.Current in DataRowVersion.Original übernommen werden. Diese Anpassungen müssen wir jedoch nicht für jede Datenzeile der Reihe nach manuell vornehmen, denn hier hilft uns die Methode AcceptChanges weiter, die auf das DataSet, eine DataTable oder eine DataRow aufgerufen werden kann. Die Änderungen wirken sich nur auf das Objekt aus, auf das die Methode aufgerufen wird.

Klappt die Aktualisierung nicht, wäre der Aufruf von AcceptChanges falsch. Wie in einem solchen Fall reagiert werden kann, behandeln wir später. Zumindest wollen wir aber sicherstellen, dass der Anwender eine Information über den misslungenen Aktualisierungsversuch erhält. Es bietet sich hier an, der Eigenschaft RowError der betreffenden DataRow eine Zeichenfolge zu übergeben, die auf den Fehlversuch hinweist. Gleichzeitig wird die Eigenschaft HasErrors=true gesetzt.


if (countDS == 1)
  row.AcceptChanges();
else
  row.RowError = "Änderung wurde nicht akzeptiert";

Das Beispielprogramm

Wir wollen uns nun ein Beispielprogramm ansehen, das unsere insgesamt sechs Methoden dazu benutzt, um Änderungen an den Datenzeilen via ExecuteNonQuery zur pubs-Datenbank zu senden. Im Code werden die Änderungen hartcodiert. Dazu wird der Datensatz des Autors White in der Weise editiert, dass er danach auf den Namen Black hört. Zudem wird ein neuer Datensatz hinzugefügt, der neben den Feldern au_id, au_lname und au_fname auch in der Spalte contract einen Wert einträgt, da diese keine NULL-Werte erlaubt.


// -----------------------------------------------------------------
// Beispiel: ...\Kapitel 26\ManuelleAktualisierungMitExecuteNonQuery
// -----------------------------------------------------------------
static void Main(string[] args) {
  // listet die Autoren in der Reihenfolge ihrer Zunamen auf
  string strSQL = "SELECT au_id, au_lname, au_fname, contract FROM authors";
  string strCon = "Data Source=(local);Initial Catalog=pubs; ";
  strCon += "Trusted_Connection=Yes";
  SqlConnection con = new SqlConnection(strCon);
  SqlDataAdapter da = new SqlDataAdapter(strSQL, con);
  DataSet ds = new DataSet();
  da.Fill(ds);
  // festlegen der Command-Objekte
  SqlCommand cmdInsert = GetInsertCommand(con);
  SqlCommand cmdUpdate = GetUpdateCommand(con);
  SqlCommand cmdDelete = GetDeleteCommand(con);
  // Datenzeilen editieren
  foreach (DataRow row in ds.Tables[0].Rows) {
    if (row["au_lname"].ToString() == "White")
      row["au_lname"] = "Black";
  }
  DataRow newRow = ds.Tables[0].NewRow();
  newRow["au_id"] = "111–11–1111";
  newRow["au_lname"] = "Fischer";
  newRow["au_fname"] = "Fritz";
  newRow["contract"] = 0;
  ds.Tables[0].Rows.Add(newRow);
  // Definition einer Variablen vom Typ DataViewRowState, 
  // die alle zu berücksichtigenden Änderungen beschreibt
  DataViewRowState drvs = DataViewRowState.Added | 
  DataViewRowState.Deleted | 
  DataViewRowState.ModifiedCurrent;
  // geänderte Datenzeilen in die Datenquelle schreiben
  int countDS = 0;
  con.Open();
  foreach (DataRow row in ds.Tables[0].Select("", "", drvs))   {
    switch (row.RowState) {
      case DataRowState.Added:
        countDS = SubmitAddedRow(cmdInsert, row);
        Console.WriteLine("DS erfolgreich hinzugefügt.");
        break;
      case DataRowState.Deleted:
        countDS = SubmitDeletedRow(cmdDelete, row);
        Console.WriteLine("DS erfolgreich gelöscht.");
        break;
      case DataRowState.Modified:
        countDS = SubmitUpdatedRow(cmdUpdate, row);
        Console.WriteLine("DS erfolgreich geändert.");
        break;
    }
    if (countDS == 1)
      row.AcceptChanges();
    else
      row.RowError = "Eine Änderung wurde nicht akzeptiert";
  }
  Console.WriteLine("Datenbank wurde aktualisiert");
  con.Close();
  Console.ReadLine();
}

Sie werden spätestens dann, wenn Sie zweimal hintereinander das Programm ausführen, einen Laufzeitfehler erhalten. Grund ist, dass zum zweiten Mal ein Datensatz mit dem gleichen Primärschlüssel hinzugefügt werden soll. Konflikte dieser Art können natürlich in der Regel nicht unbehandelt bleiben. Unser Code geht aber darauf nicht weiter ein. Weiter unten werden wir uns mit der Konfliktlösung beschäftigen.


Galileo Computing

26.7.4 Manuelles Aktualisieren mit dem DataAdapter  downtop

Änderungen an den Datenzeilen können sowohl das Löschen als auch das Hinzufügen oder Editieren einer DataRow sein. Liegen gleichzeitig verschiedenartige Änderungen vor und sollen beispielsweise nur alle gelöschten Datenzeilen der Originaldatenbank mitgeteilt werden, bietet sich der Einsatz von ExecuteNonQuery an, wie es im Abschnitt zuvor beschrieben worden ist. Sie stellen damit sicher, dass nicht alle in diesem Fall unerwünschten Änderungen gleichzeitig mitgesendet werden. Wollen Sie das DataSet aber vollständig aktualisieren, gibt es einen eleganteren und auch einfacheren Weg. Er führt über den DataAdapter und dessen Update-Methode.

Die Update-Methode arbeitet im Grunde genommen sehr ähnlich dem Code, den wir im letzten Abschnitt in Main geschrieben hatten. Sie sucht in einem DataSet bzw. in der DataTable nach den Datenzeilen, deren DataRowState nicht Unchanged ist. Trifft die Methode auf eine in welcher Weise auch immer geänderte Datenzeile, greift sie auf ein entsprechendes Command-Objekt zurück, weist die entsprechenden Parameter zu und setzt die Änderung ab.

Der entscheidende Punkt ist, dass der DataAdapter sich nicht dafür interessiert, wie das Command-Objekt gestaltet ist und aus welcher Quelle es stammt. Wichtig ist ihm nur, dass ein gültiges Command-Objekt vorliegt.

Damit haben wir auch schon den ersten Ansatz gefunden. Wir stellen eigene Command-Objekte zur Verfügung, nennen wir sie updateCommand, deleteCommand und insertCommand, und weisen sie den entsprechenden Eigenschaften des DataAdapters zu:


<SqlDataAdapter>.UpdateCommand = updateCommand;
<SqlDataAdapter>.InsertCommand = insertCommand;
<SqlDataAdapter>.DeleteCommand = deleteCommand;

Alle drei Eigenschaften sind vom Typ SqlCommand. Ein Command-Objekt kennt durch seine Eigenschaft CommandText das SQL-Kommando, das gegen die Datenbank abgesetzt werden soll. Damit sind alle Forderungen, welche die Methode Update des DataAdapters stellt, erfüllt.

Die Implementierung der Methoden

Sehen wir uns nun die Methode an, die für das Erzeugen des Kommandos zum Absetzen einer Datenzeilenänderung verantwortlich ist. Auch bei dieser Methode sei angenommen, dass zwischenzeitliche Änderungen durch andere Benutzer bei der Aktualisierung überschrieben werden. Für das Suchkriterium für den Aktualisierungsprozess genügt deshalb auch in diesem Beispiel die Angabe des Primärschlüssels.


public SqlCommand CreateUpdateCommand(SqlConnection con) {
  string strSQL = "UPDATE authors ";
  strSQL += "SET au_id = @IDNew, au_lname = @Zuname, au_fname = @Vorname ";
  strSQL += "WHERE au_id=@ID";
  SqlCommand cmd = new SqlCommand(strSQL, con);
  // die Parameter der Parameters-Auflistung hinzufügen
  SqlParameterCollection col = cmd.Parameters;
  col.Add("@IDNew", SqlDbType.VarChar, 11, "au_id");
  col.Add("@Zuname", SqlDbType.VarChar, 40, "au_lname");
  col.Add("@Vorname", SqlDbType.VarChar, 20, "au_fname");
  SqlParameter param;
  param = col.Add("@ID", SqlDbType.VarChar, 11, "au_id");
  param.SourceVersion = DataRowVersion.Original;
  return cmd;
}

Wird der DataAdapter zur Aktualisierung eingesetzt, muss jedem Parameter mitgeteilt werden, aus welcher Spalte der zu editierenden DataRow der Wert für den betreffenden Parameter abgerufen werden soll, beispielsweise:


cmd.Parameters.Add("@Vorname", SqlDbType.VarChar, 20, "au_fname"); 

Hier teilen wir dem Parameter mit, dass er den Wert aus der Spalte au_fname beziehen soll. Standardmäßig wird der Wert aus DataRowVersion.Current bezogen. Vergessen Sie die Angabe des vierten Parameters, kann der DataAdapter die Datenzeile nicht an die Datenbank übermitteln. Haben Sie eine explizite Referenz auf den Parameter, können Sie die Spalte auch der Eigenschaft SourceColumn bekannt geben.

Die Parameter der Suchkriterien benötigen den Originalwert, um die betreffende Datenzeile in der Tabelle der Datenbank aufzuspüren. Damit der DataAdapter die erforderlichen Werte aus DataRowVersion.Original einträgt, teilen Sie das dem Parameter in seiner Eigenschaft SourceVersion mit, z.B.:

param.SourceVersion = DataRowVersion.Original;

Das auf diese Weise in der Methode erzeugte Command-Objekt wird an den Aufrufer zurückgeliefert. Wie Sie weiter oben schon gesehen haben, weisen wir dessen Referenz der Eigenschaft UpdateCommand des DataAdapters zu, der automatisch die Parameter füllt, wenn er auf eine geänderte Datenzeile trifft.

In ähnlicher Weise codieren wir auch die Methoden zum Löschen und Hinzufügen von Datenzeilen.


public SqlCommand CreateDeleteCommand(SqlConnection con) {
  string strSQL = "DELETE FROM authors WHERE au_id=@ID";
  SqlCommand cmd = new SqlCommand(strSQL, con);
  // die Parameter der Parameters-Auflistung hinzufügen
  SqlParameter param;
  param = cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11,"au_id");
  param.SourceVersion = DataRowVersion.Original;
  return cmd;
}
public SqlCommand CreateInsertCommand(SqlConnection con) {
  string strSQL = "INSERT INTO authors (au_id, au_lname, au_fname, contract) ";
  strSQL += "Values(@ID, @Zuname, @Vorname, @Contract)";
  SqlCommand cmd = new SqlCommand(strSQL, con);
  // die Parameter der Parameters-Auflistung hinzufügen
  cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11, "au_id");
  cmd.Parameters.Add("@Zuname", SqlDbType.VarChar, 40,"au_lname");
  cmd.Parameters.Add("@Vorname", SqlDbType.VarChar, 20, "au_fname");
  cmd.Parameters.Add("@Contract", SqlDbType.Bit,2,"contract");
  return cmd;
}

Das Beispielprogramm

Planen Sie die manuelle Aktualisierung unter Zuhilfenahme des DataAdapters, unterscheidet sich der Programmcode kaum von dem, den Sie auch unter Benutzung des CommandBuilders schreiben würden. Anstatt den CommandBuilder zu erzeugen, weisen Sie nur den Eigenschaften InsertCommand, DeleteCommand und UpdateCommand des DataAdapters die richtigen Command-Objekte zu und rufen anschließend Update auf. Das ist bereits alles.

Das folgende Beispielprogramm zeigt Ihnen die Vorgehensweise. Wie schon im Beispiel zuvor wird ein Datensatz hinzugefügt und der Autor White umbenannt. Sie sollten, bevor Sie das Beispielprogramm ausprobieren, etwaige Änderungen, die aus dem letzten Beispiel stammen, in der Originaldatenbank zurücksetzen. Löschen Sie also den Datensatz mit der ID 111–11–1111, und lassen Sie den Autor White auch wirklich so heißen. Er könnte noch Black sein.


// --------------------------------------------------------------
// Beispiel: ...\Kapitel 26\ManuellesAktualisierenMitDataAdapter
// --------------------------------------------------------------
static void Main(string[] args) {
  // listet die Autoren in der Reihenfolge ihrer Zunamen auf
  string strSQL = "SELECT au_id, au_lname, au_fname, contract FROM authors";
  string strCon = "Data Source=(local);Initial Catalog=pubs;                    Trusted_Connection=Yes";
  SqlConnection con = new SqlConnection(strCon);
  SqlDataAdapter da = new SqlDataAdapter(strSQL, con);
  DataSet ds = new DataSet();
  da.Fill(ds);
  // Datenzeilen editieren
  foreach (DataRow row in ds.Tables[0].Rows) {
  if (row["au_lname"].ToString() == "White")
    row["au_lname"] = "Black";
  }
  // Datenzeilen hinzufügen
  DataRow newRow = ds.Tables[0].NewRow();
  newRow["au_id"] = "111–11–1111";
  newRow["au_lname"] = "Fischer";
  newRow["au_fname"] = "Fritz";
  newRow["contract"] = 0;
  ds.Tables[0].Rows.Add(newRow);
  // Festlegen der Command-Objekte
  da.InsertCommand = CreateInsertCommand(con);
  da.UpdateCommand = CreateUpdateCommand(con);
  da.DeleteCommand = CreateDeleteCommand(con);
  da.Update(ds);
}


Galileo Computing

26.7.5 Den zu aktualisierenden Datensatz in der Datenbank suchen  downtop

Ein DataSet enthält einen Teilausschnitt einer Datenbank. Sie können sich die Daten anzeigen lassen, Sie können sie aber auch ändern, neue Datensätze hinzufügen oder Datensätze löschen. Sie wissen inzwischen auch, dass Sie eine Aktualisierung auf einfache Weise mit einem CommandBuilder in die Wege leiten können. Das ist ausgesprochen simpel zu programmieren und funktioniert tadellos. Ich habe Ihnen zudem gezeigt, wie Sie ohne den CommandBuilder eine eigene Aktualisierungslogik bereitstellen können. Der Code ist vielleicht spannend, aber andererseits könnten Sie auch zu der Erkenntnis kommen, dass die investierte Zeit nicht besonders effektiv genutzt worden ist.

Dem ist bei weitem nicht so. Wahrscheinlich werden Sie sogar sehr häufig eine eigene Aktualisierungslogik generieren müssen. Ich möchte Ihnen das am Beispiel des CommandBuilders beweisen. Gehen wir davon aus, dass einer Abfrage das folgende SELECT-Statement zugrunde liegt:


SELECT au_id, au_lname, au_fname FROM authors

Auf dieser Grundlage wird der CommandBuilder ein UpdateCommand erzeugen, das wie folgt aussieht:


UPDATE authors SET au_id=@p1, au_lname=@p2, au_fname=@p3 
WHERE au_id=@p4 AND au_lname=@p5 AND au_fname=@p6

In der WHERE-Klausel der UPDATE-Anweisung sind alle Spalten enthalten, die in SELECT angegeben worden sind. Die Parameter @p4 bis @p6 werden bei der Aktualisierung mit den Werten aus DataRowVersion.Original gefüttert.

Nehmen wir nun an, der Benutzer der Datenbankanwendung hätte einen Datensatz mit den folgenden Feldinhalten bezogen:


au_id = "123–12–1234"
au_lname = "Müller"
au_fname = "Peter"

Ändert der Anwender den Zunamen in »Meier«, wird der CommandBuilder folgendes UPDATE-Statement erzeugen:


UPDATE authors SET au_id="123–12–1234", au_lname="Meier", au_fname="Peter" 
WHERE au_id=@p4 AND au_lname="Müller" AND au_fname="Peter"

Auf der Datenbank wird nun ein Datensatz in der Autorentabelle gesucht, der genau den Kriterien entspricht, welche die WHERE-Klausel beschreibt. Wird der Datensatz gefunden, werden in den in SET aufgeführten Spalten die entsprechenden Werte eingetragen. Die Aktualisierung war erfolgreich.

Zu glauben, dass das immer so sein wird, ist utopisch, wenn mehrere Anwender ihre Änderungen an die Datenbank übermitteln können. Nehmen wir an, Anwender A ändert wie gezeigt in seinem DataSet den Datensatz, während Anwender B ebenfalls eine Änderung am gleichen Datensatz vornimmt, jedoch nicht in der Spalte au_lname, sondern in der Spalte au_fname. Aktualisiert Anwender B zuerst, werden seine editierten Daten problemlos in die Datenbank geschrieben. Versucht anschließend Anwender A, seine Änderungen zu übermitteln, schlägt der Versuch fehl, weil der Datensatz, der überschrieben werden soll, nicht mehr gefunden wird.

Merken Sie, worauf meine Ausführungen hinauslaufen? Sie müssen schon im Vorfeld der Entwicklung klären, wie Sie unter Berücksichtigung mehrerer Benutzer die Datenbank aktualisieren wollen. Je nach Ausgangssituation und den angegebenen Suchkriterien werden die Daten entweder erfolgreich in die Datenbank geschrieben, oder die Aktualisierung verursacht einen Konflikt.

Grundsätzlich können wir hinsichtlich der Kriterien zur Identifizierung eines zur Aktualisierung anstehenden Datensatzes zwischen drei Fällen unterscheiden:

gp  Die WHERE-Klausel enthält alle Spalten der SELECT-Abfrage.
gp  Die WHERE-Klausel enthält die Primärschlüsselspalte.
gp  Die WHERE-Klausel enthält nur die Angabe der geänderten Spalten und der Primärschlüsselspalte.

Wir sollten uns nun mit den Konsequenzen dieser drei Varianten vertraut machen, denn die Entscheidung hat auch Einfluss darauf, wie wir mit den eventuell auftretenden Konflikten umgehen und sie lösen.

Die WHERE-Klausel enthält alle Spalten

Standardmäßig nimmt der CommandBuilder alle Spalten in der WHERE-Klausel auf, die mit SELECT abgefragt worden sind. Die Folge ist, dass der Code keine Änderungen in einer DataRow überschreiben kann, die zwischenzeitlich von anderen Anwendern vorgenommen worden sind.

Dazu ein Beispiel. Angenommen, Anwender A und Anwender B rufen die gleiche Datenzeile in der Autorentabelle ab. Ändert Anwender A die Spalte au_lname, werden alle Spalten der Abfrage in die WHERE-Klausel aufgenommen. Das UPDATE-Statement könnte dann beispielsweise wie folgt aussehen:


UPDATE authors
SET au_id="172–32–1176", au_lname="Black", city="Oakland"
WHERE au_id="172–32–1176", au_lname="White", city="Oakland"

In der Zwischenzeit könnte auch Anwender B die Datenzeile mit dem Autor geändert haben, jedoch nicht wie Anwender A die Spalte au_lname, sondern die Spalte city.


UPDATE authors
SET au_id="172–32–1176", au_lname="White", city="New York"
WHERE au_id="172–32–1176", au_lname="White", city="Oakland"

Hat Anwender A seine Änderungen zuerst übermittelt, scheitert der Aktualisierungsversuch von Anwender B, weil keine Zeile in der Tabelle den Kriterien der WHERE-Klausel entspricht.

Bei diesem Szenario »gewinnt« immer der Anwender, der als erster seine Änderungen an die Datenbank übermittelt. Der Anwender, der seine Änderungen später zur Datenbank schickt, hat das Nachsehen. Sein Aktualisierungsversuch misslingt.

Die WHERE-Klausel enthält die Primärschlüsselspalte

Aktualisierungsszenarien, die anders ablaufen sollen als das zuvor beschriebene, erfordern das Bereitstellen benutzerdefinierter Command-Objekte. Ein denkbarer Ansatz wäre der, dass jede Aktualisierung grundsätzlich immer in den entsprechenden Datensatz der Datenbank geschrieben wird. Zwischenzeitliche Änderungen durch andere Anwender werden ohne Kenntnis darüber, was ein zweiter Anwender geändert hat, überschrieben. Deshalb wird diese Art der Aktualisierung auch als »Last in wins« bezeichnet.

Bei dieser Form der Aktualisierung muss nur der betroffene Datensatz eindeutig identifiziert werden. Deshalb wird hinter der WHERE-Klausel auch nur der Primärschlüssel der Datenzeile angegeben.

Halten wir uns nun die Situation vor Augen. Wieder helfen uns die beiden fiktiven Anwender A und B dabei, den Sachverhalt zu verstehen. Beide Anwender rufen parallel die gleiche Datenzeile ab und nehmen Änderungen an einer der Spalten vor. Anwender A aktualisiert die Originaldatenbank zuerst. Wie das UPDATE-Statement aussieht, spielt dabei noch nicht einmal eine Rolle. Anwender B übermittelt seine Änderung erst später. Nehmen wir an, Anwender B hätte den Inhalt in der Spalte city verändert, könnte sein vollständiges Aktualisierungsstatement wie folgt lauten:


UPDATE authors
SET au_id="172–32–1176", au_lname="White", city="New York"
WHERE au_id="172–32–1176"

Die Aktualisierung wird erfolgreich sein, wenn der Datensatz mit der angegebenen au_id in der Tabelle gefunden wird. Die Änderungen von Anwender A sieht Anwender B nicht und wird vielleicht auch niemals erfahren, welche Daten Anwender A überschrieben hat. Die Identifizierung der zu ändernden Datenzeile nur anhand der Primärschlüsselspalte ist folglich auch denkbar ungeeignet, wenn Sie vermeiden müssen, dass Anwender B unwissentlich geänderte Daten überschreibt. Können Sie davon ausgehen, dass die letzte Aktualisierung zweifelsfrei diejenige mit den »besten« Daten ist, sollten Sie sich für diese Alternative entscheiden.

Angabe der geänderten Spalten

Die beiden vorhergehend beschriebenen Szenarien sind ausgesprochen gegensätzlich: Entweder wird eine vorhergehende Aktualisierung kompromisslos überschrieben, oder der Anwender, der als zweiter versucht, seine Änderungen zu übermitteln, hat sich mit einem Konflikt auseinander zu setzen.

Dadurch, dass Sie mit ADO.NET selbst die Aktualisierungslogik bereitstellen können, können Sie auch einen Kompromiss schließen und in der WHERE-Klausel neben der Primärschlüsselspalte auch die Spalten angeben, die der Anwender selbst verändert hat.

Eine kurze Beschreibung der Situation. Anwender A und Anwender B rufen gleichzeitig die gleiche Zeile mit Autorendaten ab. Anwender A ändert die Spalte au_lname, Anwender B die Spalte city. Anwender A übermittelt als erster seine Änderungen. Das UPDATE-Statement wie folgt aus:


UPDATE authors
SET au_lname="Black"
WHERE au_id="172–32–1176" AND au_lname="White",

Bei den Spalteninhalten, die hinter WHERE aufgeführt sind, handelt es sich um die Originaldaten, die der Anwender beim Füllen des DataSets erhalten hat.

Die UPDATE-Abfrage, die Anwender B nach Anwender A an die Datenbank sendet, sucht aber nach anderen Gesichtspunkten den zu aktualisierenden Datensatz:


UPDATE authors
SET city="Miami"
WHERE au_id="172–32–1176" AND city="Oakland"

Der Datensatz kann auch bei der später erfolgenden Aktualisierung immer noch eindeutig in der Tabelle der Autoren identifiziert werden. Damit werden auch die Änderungen, die Anwender B vorgenommen hat, in die Tabelle geschrieben, ohne dass die Änderungen von Anwender A überschrieben werden. Beide kommen zu ihrem Recht, solange sie unterschiedliche Spalten editiert haben.

Es sollte an dieser Stelle erwähnt werden, dass die Aktualisierungslogik des Command-Objekts dynamisch zur Laufzeit erzeugt werden muss, da Sie nicht wissen, welche Spalten von den Änderungen des jeweiligen Benutzers betroffen sind. Hierzu vergleichen Sie DataRowVersion.Current und DataRowVersion.Original jeder Spalte einer Datenzeile und »schrauben« sich auf diese Weise die Zeichenfolge des UDATE-Befehls zusammen, bevor Sie das Command–Objekt damit füttern. Die Codierung dazu kann, je nachdem, wie viele Spalte die Datenzeile hat, relativ aufwändig werden. Das ist der große Nachteil dieser Aktualisierung.

Fazit

Einen Tipp zu geben, welche Suchkriterien zu bevorzugen sind, ist nicht möglich. Sie müssen genau analysieren, welche Vorstellungen Sie oder Ihr Kunde haben. Wahrscheinlich werden Sie damit sogar heftige Diskussionen auslösen, bevor die endgültige Entscheidung fällt. Aber das liegt in der Natur der Sache, denn jede Datenbank ist spezifisch, und jede Datenbankanwendung muss anderen Kriterien genügen.

Wichtig ist es zu wissen, dass Sie mit ADO.NET ein Werkzeug in den Händen halten, das alle Möglichkeiten offen lässt. Wenn Sie Glück haben, genügt die Aktualisierungslogik des CommandBuilders den Ansprüchen. Dann wird die Aktualisierung zu einem Kinderspiel. Wenn nicht, erzeugen Sie ein UPDATE-Statement nach den Vorgaben, die an die Anwendung gestellt werden.


Galileo Computing

26.7.6 Den Benutzer über die fehlgeschlagenen Aktualisierungen informieren  downtop

Stehen mehrere Datenzeilen zur Aktualisierung an, wird der DataAdapter versuchen, eine nach der anderen an die Datenbank zu senden. Wie aber wird sich der DataAdapter verhalten, wenn eine der Datenzeilen einen Konflikt verursacht? Der DataAdapter wird eine DBConcurrencyException auslösen und die verbleibenden Änderungen nicht mehr an die Datenbank schicken. So ist das Standardverhalten.

Sie können den DataAdapter auch anweisen, nach einem etwaigen Konflikt seine Aufgabe fortzusetzen und auch die verbleibenden Änderungen zu übermitteln. Dazu setzen Sie die Eigenschaft ContinueUpdateOnError=true. Das hat weit reichende Konsequenzen, denn nun verursacht ein fehlgeschlagener Aktualisierungsversuch keine Ausnahme mehr. Stattdessen wird die Eigenschaft HasErrors des entsprechenden DataRow-Objekts auf true gesetzt, ebenso die gleichnamige Eigenschaft des DataSets und der DataTable. Eine DataRow hat auch eine Eigenschaft RowError. Diese enthält nach dem misslungenen Versuch eine Fehlermeldung.

Im folgenden Beispielcode wird der Einsatz der Eigenschaften ContinueUpdateOnError, HasErrors und RowError gezeigt. Die Command-Objekte werden von den schon bekannten Methoden CreateUpdateCommand, CreateDeleteCommand und CreateInsertCommand generiert. Um eine etwas größere »Spielwiese« zu haben, ist das Suchkriterium von CreateUpdateCommand um eine Spalte (au_lname) ergänzt worden.

Geändert wird erneut der Datensatz des Autors White in der Tabelle authors. Sorgen Sie deshalb vor dem ersten Ausführen dafür, den Ursprungszustand der Tabelle wieder herzustellen. Außerdem wird erneut ein Datensatz zur Tabelle hinzugefügt. In Main wird die laufende Anwendung nach dem Füllen des DataSets unterbrochen. Das ist der Moment, um einen zweiten Client zu simulieren, der ebenfalls den Datensatz des Autors White editiert. Sie können dazu das Kompilat aus dem Windows Explorer heraus starten.


Hinweis   Sollten Sie die Vollversion von SQL Server 2000/2005 installiert haben, können Sie die Daten auch im »Enterprise Manager« bzw. im »SQL Server Management Studio« ändern.

Ändern Sie dabei die Spalte au_lname oder au_id, provozieren Sie den Parallelitätskonflikt. Es erscheint an der Konsole eine Fehlermeldung. Editieren Sie jedoch eine andere Spalte, werden beide Aktualisierungen von der Datenbank akzeptiert.


// --------------------------------------------------------------
// Beispiel: ...\Kapitel 26\Konfliktbeschreibung
// --------------------------------------------------------------
static void Main(string[] args) {
  // listet die Autoren in der Reihenfolge ihrer Zunamen auf
  string strSQL = "SELECT au_id, au_lname, au_fname, contract FROM authors";
  string strCon = "...";
  SqlConnection con = new SqlConnection(strCon);
  SqlDataAdapter da = new SqlDataAdapter(strSQL, con);
  DataSet ds = new DataSet();
  da.Fill(ds);
  // Hier wird angehalten, um einen Konflikt zu provozieren
  Console.Write("Ändern Sie jetzt in der Originaldatenbank.");
  Console.WriteLine("Weiter mit <Enter>.");
  Console.ReadLine();
  // Datenzeilen editieren
  foreach (DataRow row in ds.Tables[0].Rows) {
    if (row["au_lname"].ToString() == "White")
      row["au_lname"] = "Black";
  }
  DataRow newRow = ds.Tables[0].NewRow();
  newRow["au_id"] = "111–11–1111";
  newRow["au_lname"] = "Fischer";
  newRow["au_fname"] = "Fritz";
  newRow["contract"] = 0;
  ds.Tables[0].Rows.Add(newRow);
  // Festlegen der Command-Objekte
  da.InsertCommand = CreateInsertCommand(con);
  da.UpdateCommand = CreateUpdateCommand(con);
  da.DeleteCommand = CreateDeleteCommand(con);
  da.ContinueUpdateOnError = true;
  da.Update(ds);
  // Prüfen, ob ein Konflikt aufgetreten ist
  if (ds.HasErrors) {
    string message = "Folgende Zeilen konnten nicht aktualisiert werden:";
    foreach (DataRow row in ds.Tables[0].Rows)
      if (row.HasErrors) {
        Console.WriteLine(message);
        Console.WriteLine("ID: {0}, Fehler: {1}", row["au_id"], row.RowError);
      }
  }
  else
    Console.WriteLine("Die Aktualisierung war erfolgreich.");
  Console.ReadLine();
}
public static SqlCommand CreateUpdateCommand(SqlConnection con) {
  string strSQL = "UPDATE authors ";
  strSQL += "SET au_id=@IDNew,au_lname=@Zuname, au_fname=@Vorname ";
  strSQL += "WHERE au_id=@ID AND au_lname=@ZunameOld";
  SqlCommand cmd = new SqlCommand(strSQL, con);
  // die Parameter der Parameters-Auflistung hinzufügen
  SqlParameterCollection col = cmd.Parameters;
  col.Add("@IDNew", SqlDbType.VarChar, 11, "au_id");
  col.Add("@Zuname", SqlDbType.VarChar, 40, "au_lname");
  col.Add("@Vorname", SqlDbType.VarChar, 20, "au_fname");
  SqlParameter param;
  param = col.Add("@ID", SqlDbType.VarChar, 11, "au_id");
  param.SourceVersion = DataRowVersion.Original;
  param = col.Add("@ZunameOld", SqlDbType.VarChar, 40, "au_lname");
  param.SourceVersion = DataRowVersion.Original;
  return cmd;
}

Nach dem Aufruf von Update auf den DataAdapter wird zuerst mit


if (ds.HasErrors)  

das DataSet dahingehend untersucht, ob überhaupt ein Konflikt vorliegt. HasErrors ist false, wenn die Datenbank die Änderungen angenommen hat. true signalisiert hingegen, dass wir alle Datenzeilen in der Tabelle des DataSets durchlaufen müssen, um die konfliktverursachenden Zeilen zu finden. Fündig wird der Code, wenn er auf eine Datenzeile mit HasErrors=true trifft.


foreach (DataRow row in ds.Tables[0].Rows)
  if (row.HasErrors) {
   ...
  }

Jetzt können wir reagieren. Im einfachsten Fall lassen wir uns zumindest die ID des betreffenden Übeltäters ausgeben – so wie in diesem Beispiel. Sie können die Information natürlich auch dazu benutzen, dem Benutzer die Chance zu geben, die Konfliktursache zu beseitigen, denn der Verursacher ist ermittelt.


Galileo Computing

26.7.7 Die konfliktverursachenden Datenzeilen bei der Datenbank abfragen  toptop

Meist genügt es nicht nur zu wissen, wer Konfliktverursacher ist. Es wird darüber hinaus auch eine Lösung angestrebt. Das bedarf aber einer genaueren Analyse der Umstände. Ich möchte Ihnen das kurz erläutern.

Wird versucht, einen bereits vorhandenen Primärschlüssel für einen neuen Datensatz ein zweites Mal zu vergeben, scheint die Lösung des Problems noch recht einfach zu sein: Es muss dann nur ein anderer Primärschlüssel vergeben werden. Aber das könnte eine falsche Entscheidung sein. Können Sie denn sicherstellen, dass nicht zwei Anwender versuchen, den gleichen Datensatz zur Tabelle hinzuzufügen? Wenn Sie diese Situation nicht berücksichtigen, liegen im schlimmsten Fall zwei identische Datensätze vor.

Weiterhin stellt sich auch die Frage, warum eine geänderte Datenzeile nicht erfolgreich aktualisiert werden konnte. Die Ursache könnte sein, dass ein anderer Anwender seinerseits den Datensatz geändert hat, es könnte aber auch sein, dass der Datensatz gar nicht mehr existiert, weil er in der Originaltabelle gelöscht worden ist.

Vielleicht merken Sie, es fehlt uns bisher an Informationen, um präzise Lösungen erarbeiten und codieren zu können. Der Ansatz zur Lösungsfindung ist nicht im aktuellen DataSet zu finden, sondern in der Datenbank selbst. Was wir brauchen, ist eine neue Originalversion der konfliktverursachenden Datenzeile.

Der DataAdapter hilft uns weiter. Er löst nämlich für jede zu aktualisierende Datenzeile zwei Ereignisse aus, wenn anstehende Änderungen über die Methode Update an die Datenbank übermittelt werden:

gp  RowUpdating
gp  RowUpdated

RowUpdating wird ausgelöst, bevor eine Zeile übermittelt wird, RowUpdated tritt unmittelbar nach der Übermittlung auf. Für unsere Lösung untersuchen wir im Ereignishandler nach der Übermittlung den zweiten Parameter vom Typ SqlRowUpdatedEventArgs. Mehrere Eigenschaften dieses Typs unterstützen uns bei unserem weiteren Vorgehen.


Tabelle 26.8   Die Eigenschaften des »SqlRowUpdatedEventArgs«-Objekts

Eigenschaft Beschreibung
Command Ruft das beim Aufruf von Update ausgeführte SqlCommand ab.
Errors Ruft alle Fehler ab, die während der Ausführung generiert wurden.
RecordsAffected Ruft die Anzahl der durch die Ausführung der SQL-Anweisung geänderten, eingefügten oder gelöschten Zeilen ab.
Row Ruft die durch ein Update gesendete DataRow ab.
StatementType Ruft den Typ der ausgeführten SQL-Anweisung ab.
Status Ruft einen Wert der Enumeration UpdateStatus ab oder legt diesen fest.
TableMapping Ruft das durch ein Update gesendete DataTableMapping ab.

Der Vollständigkeit halber folgt jetzt auch noch die Tabelle mit den Membern der Enumeration UpdateStatus, die von der Eigenschaft Status des SqlRowUpdatedEventArgs-Objekts offen gelegt wird.


Tabelle 26.9   Die Member der Enumeration »UpdateStatus«

Member Beschreibung
Continue Der DataAdapter soll mit der Verarbeitung von Zeilen fortfahren.
ErrorsOccured Der Ereignishandler meldet, dass die Aktualisierung als Fehler behandelt werden soll.
SkipAllRemainingRows Die aktuelle Zeile und alle restlichen Zeilen sollen nicht aktualisiert werden.
SkipCurrentRow Die aktuelle Zeile soll nicht aktualisiert werden.

Wie können wir nun das Ereignis zu unserem Nutzen einsetzen?

Es gilt zunächst einmal festzustellen, ob das Updaten einer Datenzeile zu einem Konflikt geführt hat. Hierzu prüfen wir, ob die Eigenschaft Status des SqlRowUpdatedEventArgs-Objekts den Enumerationswert UpdateStatus.ErrorsOccured aufweist.


private void da_RowUpdated(object sender, SqlRowUpdatedEventArgs e) {
  if (e.Status == UpdateStatus.ErrorsOccurred) {
    ...
  }
}

Alle konfliktverursachenden Datenzeilen können in einem DataSet zusammengefasst werden, das sich nach kompletter Aktualisierung auswerten lässt. Dazu muss der aktuelle Stand der konfliktverursachenden Datenzeilen aus der Originaldatenbank bezogen werden, damit Benutzer oder Programmcode die Basis zu einer möglichen Entscheidung hinsichtlich einer Konfliktlösung haben. Im Ereignishandler wird deshalb eine parametrisierte Abfrage, die gegen die Datenbank abgesetzt werden soll, zusammengeschraubt (aber zunächst noch nicht ausgeführt). Sinnvollerweise wird dabei als Parameter nur der Primärschlüssel der konfliktverursachenden Datenzeile verwendet. Abgefragt werden von der Datenbank die Inhalte aller im Zusammenhang mit der Aktualisierung interessierenden Spalten.


string strSQL = "SELECT au_id, au_lname, au_fname, contract ";
strSQL += "FROM authors WHERE au_id=@ID";
SqlCommand cmd = new SqlCommand(strSQL, con);
cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11, "au_id");
daConflict.SelectCommand = cmd;
daConflict.SelectCommand.Parameters[0].Value = e.Row["au_id"];

daConflict ist hierbei die Referenz auf ein SqlDataAdapter-Objekt.

Grundsätzlich können beim Aktualisieren einer Datenzeile zwei verschiedene Exceptions auftreten:

gp  SqlException
gp  DBConcurrencyException

SqlException beschreibt eine Ausnahme, wenn SQL Server einen Fehler zurückgibt. Das wäre beispielsweise der Fall, wenn ein Datensatz hinzugefügt wird mit einem Primärschlüssel, der in der Tabelle bereits existiert.

DBConcurrencyException hingegen wird ausgelöst, wenn eine Parallelitätsverletzung vorliegt. Das ist der Fall, wenn die Anzahl der zurückgegebenen Datenzeilen 0 ist.

Um festzustellen, welche Ausnahme ausgelöst worden ist, brauchen Sie nur den in der Eigenschaft Errors des SqlRowUpdatedEventArgs-Objekts enthaltenen Ausnahmetyp zu untersuchen. Ist dieser bekannt, können Sie die parametrisierte Abfrage starten. Die Anzahl der zurückgelieferten Datenzeilen, es kann sich dabei nur um eine oder keine handeln, lässt weitere Rückschlüsse auf das vorherige Scheitern der Aktualisierung zu.

Betrachten wir zuerst den Fall, dass Errors ein SqlException-Objekt enthält.


if (e.Errors.GetType() == typeof(SqlException))
{
  if (daConflict.Fill(dsConflict) == 1)
    Console.WriteLine(e.Row.RowError = "Der PS existiert bereits.");
}

Es wird die Fill-Methode des DataAdapters aufgerufen, um das Konfliktdataset zu füllen. Die parametrisierte Abfrage enthält dabei in ihrem Parameter @ID den Primärschlüssel der Datenzeile, die den Konflikt verursacht hat. Zur Erinnerung, es handelt sich dabei um den Primärschlüssel eines Datensatzes, der neu hinzugefügt werden soll, wobei der Verdacht nahe liegt, dass der Primärschlüssel bereits in der Originaltabelle vergeben ist. Wird die parametrisierte Abfrage eine Datenzeile zurückliefern, bestätigt sich der Verdacht.

Handelt es sich in Errors um DBConcurrencyException, wurde der Versuch, die Änderung an einer Datenzeile in die Originaltabelle zu schreiben, abgelehnt. Die Parallelitätsverletzung kann zwei Ursachen haben:

gp  Ein anderer Anwender hat den Datensatz zwischenzeitlich geändert.
gp  Der Datensatz wurde von einem anderen Anwender gelöscht.

Festzustellen, welcher der beiden Punkte zum Konflikt geführt hat, ist sehr einfach, denn nach dem Absetzen der parametrisierten Abfrage muss nur der Rückgabewert untersucht werden. Ist das Ergebnis 0, d. h., der entsprechende Datensatz wurde anhand des Primärschlüssels nicht gefunden, existiert er nicht mehr, er wurde gelöscht.


if (e.Errors.GetType() == typeof(DBConcurrencyException)) {
  if (daConflict.Fill(dsConflict) == 1)
    Console.WriteLine(e.Row.RowError = "Datensatz wurde geändert.");
  else
    Console.WriteLine("Datensatz existiert nicht in der Datenbank.");

Wie mit dem Inhalt des DataSets umgegangen wird, das den aktuellen Stand der konfliktverursachenden Zeilen enthält, richtet sich nach den individuellen Bedürfnissen des Kunden, der die Anwendung einsetzt. Die Lösung kann sehr unterschiedlich ausfallen und dabei auch noch sehr komplex sein. Wir wollen daher an dieser Stelle darauf verzichten und uns stattdessen die entscheidenden Ansätze in einem Beispielprogramm im Zusammenhang ansehen.


// --------------------------------------------------------------
// Beispiel: ...\Kapitel 26\KonfliktDataset
// --------------------------------------------------------------
class Program {
  static SqlDataAdapter daConflict = new SqlDataAdapter();
  static DataSet dsConflict = new DataSet();
  static SqlConnection con;
  static void Main(string[] args) {
    // listet die Autoren in der Reihenfolge ihrer Zunamen auf
    string strSQL = "SELECT au_id, au_lname, au_fname, contract FROM authors";
    string strCon = "...";
    con = new SqlConnection(strCon);
    SqlDataAdapter da = new SqlDataAdapter(strSQL, con);
    DataSet ds = new DataSet();
    da.Fill(ds);
    // Datenzeilen editieren
    foreach (DataRow row in ds.Tables[0].Rows) {
      if (row["au_lname"].ToString() == "White")
        row["au_lname"] = "Black";
    }
    DataRow newRow = ds.Tables[0].NewRow();
    newRow["au_id"] = "111–11–1111";
    newRow["au_lname"] = "Fischer";
    newRow["au_fname"] = "Fritz";
    newRow["contract"] = 0;
    ds.Tables[0].Rows.Add(newRow);
    // Aktualisierung vorbereiten
    Console.WriteLine("Jetzt das au_lname-Feld des Datensatzes im                         SQL-Server ändern.");
    Console.ReadLine();
    da.UpdateCommand = CreateUpdateCommand(con);
    da.InsertCommand = CreateInsertCommand(con);
    da.DeleteCommand = CreateDeleteCommand(con);
    da.ContinueUpdateOnError = true;
    da.RowUpdated += new SqlRowUpdatedEventHandler(da_RowUpdated);
    da.Update(ds);
    Console.ReadLine();
  }
  // Wird beim Versuch der Datensatzänderung ausgelöst
  static void da_RowUpdated(object sender, SqlRowUpdatedEventArgs e) {
    if (e.Status == UpdateStatus.ErrorsOccurred) {
      // Vorbereitung auf die Datenbankabfrage
      string strSQL = "SELECT au_id, au_lname, au_fname, contract ";
      strSQL += "FROM authors WHERE au_id=@ID";
      SqlCommand cmd = new SqlCommand(strSQL, con);
      cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11, "au_id");
      daConflict.SelectCommand = cmd;
      daConflict.SelectCommand.Parameters[0].Value = e.Row["au_id"];
      if (e.Errors != null)
        if (e.Errors.GetType() == typeof(SqlException)) {
          // Prüfen, ob es einen DS mit einem bestimmten PS gibt
          if (daConflict.Fill(dsConflict) == 1)
            Console.WriteLine(e.Row.RowError = "Der PS existiert bereits.");
        }
        else if (e.Errors.GetType() == typeof(DBConcurrencyException)) {
          // ist Anzahl=1 -> anderer Benutzer hat DS geändert
          if (daConflict.Fill(dsConflict) == 1)
            Console.WriteLine(e.Row.RowError = "Datensatz wurde geändert.");
          else
            // anzahl=0 bedeutet, dass der Datensatz gelöscht worden ist
            Console.WriteLine("Datensatz existiert nicht in der Datenbank.");
        }
    }
  }
}

In Main werden zuerst einige Daten aus der Autorentabelle der pubs-Datenbank abgefragt und einer der Datensätze editiert. Zudem wird ein neuer Datensatz hinzugefügt. Die benutzerdefinierten Methoden CreateUpdateCommand, CreateDeleteCommand und CreateInsertCommand sind hinlänglich bekannt und werden deshalb hier nicht noch einmal wiedergegeben.

Wenn Sie das Beispielprogramm ausprobieren, sollten Sie zuerst sicherstellen, dass es den Autor White auch tatsächlich gibt. Wir haben weiter oben nämlich schon des Öfteren dessen Namen geändert. Nach dem Start wird eine Konsolenausgabe angezeigt, in der Sie dazu aufgefordert werden, das Feld au_lname des Autors White zu ändern. Diese Änderung nehmen Sie in einer anderen Anwendung vor, beispielsweise im »SQL Server Enterprise Manager« von SQL Server 2000 oder dem »Management Studio« von SQL Server 2005. Damit provozieren Sie einen Parallelitätskonflikt, der auch als solcher an der Konsole beschrieben wird. Der erneute, zweite Start des Beispielprogramms wird schließlich dazu führen, dass der Versuch, den Datensatz mit dem Primärschlüssel 111–11–1111wiederholt in die Datenbank zu schreiben, scheitert.

 << zurück
  
  Zum Katalog
Zum Katalog: Visual C# 2005
Visual C# 2005
bestellen